/*
    RoleCall provides a web service that web applications can use to find 
    the roles associated with a user that has been authenticated via OpenID. 
    Also provides a web-based user interface to manage the user roles 
    associated with partner web applications.
    
    Development of this software was supported in part by the David and Lucile 
    Packard Foundation and by the the Office Of Naval Research (ONR) 
     
    Copyright (c) 2012, 
    Monterey Bay Aquarium Research Institute - MBARI ( www.mbari.org )
    Michael Godin ( mikegodin AT users DOT sourceforge DOT net )

    RoleCall is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    RoleCall is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Foobar (see the file COPYING.GPL).  If not, see 
    <http://www.gnu.org/licenses/>.  
*/

package org.mbari.rolecall.server;

import java.io.IOException;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import org.mbari.rolecall.model.Relay;
import org.mbari.rolecall.model.RelayRole;
import org.mbari.rolecall.model.Role;
import org.mbari.rolecall.model.Usage;
import org.mbari.rolecall.model.User;
import org.mbari.rolecall.model.UserRole;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

public class RoleCallServer {

    protected static final BASE64Decoder base64Dec = new BASE64Decoder();

    protected static final BASE64Encoder base64Enc = new BASE64Encoder();

    public static String encrypt(String publicKey, final String plainText)
            throws IOException {
        byte[] pkBytes = base64Dec.decodeBuffer(publicKey);
        // System.out.println("pkBytes=" + new String(Hex.encodeHex(pkBytes)));
        int modBitLen = pkBytes[0] * 256 + pkBytes[1];
        int modByteLen = (modBitLen + 7) / 8;
        // System.out.println("modByteLen=" + modByteLen);
        byte[] modBytes = Arrays.copyOfRange(pkBytes, 2, modByteLen + 2);
        // System.out.println("modBytes=" + new
        // String(Hex.encodeHex(modBytes)));
        BigInteger mod = new BigInteger(1, modBytes);
        // System.out.println("modBitLen=" + modBitLen + ",mod.bitLength()="
        // + mod.bitLength());
        // System.out.println("mod=" + mod.toString(16));
        int expBitLen = pkBytes[modByteLen + 2] * 256 + pkBytes[modByteLen + 3];
        int expByteLen = (expBitLen + 7) / 8;
        // System.out.println("expByteLen=" + expByteLen);
        byte[] expBytes = Arrays.copyOfRange(pkBytes, modByteLen + 4,
                modByteLen + expByteLen + 4);
        // System.out.println("expBytes=" + new
        // String(Hex.encodeHex(expBytes)));
        BigInteger exp = new BigInteger(1, expBytes);
        // System.out.println("expBitLen=" + expBitLen + ",exp.bitLength()="
        // + exp.bitLength());
        // System.out.println("exp=" + exp.toString(16));
        int charsperchunk = (mod.bitLength() + 7) / 8;

        byte[] plainBytes = plainText.getBytes();
        int chunks = (plainBytes.length + charsperchunk - 1) / charsperchunk;
        byte[] encodedBytes = new byte[charsperchunk * chunks];
        for (int i = 0; i < chunks; i++) {
            byte[] chunkBytes = Arrays.copyOfRange(plainBytes, i
                    * charsperchunk, (i + 1) * charsperchunk);
            // System.out.println("chunkBytes="
            // + new String(Hex.encodeHex(chunkBytes)));
            BigInteger encodedChunk = new BigInteger(chunkBytes).modPow(exp,
                    mod);
            byte[] encodedChunkBytes = encodedChunk.toByteArray();
            // System.out.println("encodedChunkBytes="
            // + new String(Hex.encodeHex(encodedChunkBytes)));
            // System.out.println("encodedChunkBytes.length="
            // + encodedChunkBytes.length + ",charsperchunk="
            // + charsperchunk);
            int offset = encodedChunkBytes.length > charsperchunk ? 1 : 0;
            System.arraycopy(encodedChunkBytes, offset, encodedBytes, i
                    * charsperchunk, charsperchunk);
        }
        // System.out.println("encodedBytes="
        // + new String(Hex.encodeHex(encodedBytes)));
        return base64Enc.encode(encodedBytes);
    }

    public static String getRoles(String email, String openId,
            String roleCallRelayIdStr, String remoteAddr) throws IOException {
        Integer roleCallRelayId;
        try {
            roleCallRelayId = Integer.valueOf(roleCallRelayIdStr);
        } catch (NumberFormatException nfe) {
            return "Error: invalid roleCallRelayId sent to RoleCall: "
                    + roleCallRelayIdStr;
        }
        Relay relay = DB.getRelay(roleCallRelayId, remoteAddr);
        if (null == relay) {
            return "Error: RoleCall could not find relay with id="
                    + roleCallRelayId + " at address " + remoteAddr;
        }

        User user = DB.getUser(email, openId);
        if (null == user) {
            user = new User(email, openId);
            DB.persist(user);
        }

        Usage usage = DB.getUsage(user, relay);
        if (null == usage) {
            usage = new Usage(new Date(), user, relay);
        } else {
            usage.setLastDate(new Date());
        }
        DB.persist(usage);

        String publicKey = relay.getPublicKey();
        if (!relay.isAdminRoleAssigned()) {
            Role adminRole = DB.getRole(relay.getAdminRoleName());
            UserRole adminUserRole = null == adminRole ? null : new UserRole(
                    user, relay, adminRole);
            Role roleCallRelayAdminRole = DB.getRole("roleCallRelayAdmin");
            if (null == roleCallRelayAdminRole) {
                roleCallRelayAdminRole = new Role("roleCallRelayAdmin");
                DB.persist(roleCallRelayAdminRole);
            }
            UserRole roleCallRelayAdminUserRole = new UserRole(user, relay,
                    roleCallRelayAdminRole);
            if ((null == adminUserRole || DB.persist(adminUserRole))
                    && DB.persist(roleCallRelayAdminUserRole)) {
                relay.setAdminRoleAssigned(true);
                DB.persist(relay);
            }
        }
        Set<String> userRoleNameSet = new HashSet<String>();
        DB.addUserRoleNamesToSet(user, relay, userRoleNameSet);
        DB.addUserRoleMatchNamesToSet(email, relay, userRoleNameSet);
        StringBuilder roleStr = new StringBuilder(
                Double.toString(Math.random()));
        for (String userRoleName : userRoleNameSet) {
            roleStr.append(",");
            roleStr.append(userRoleName);
        }
        return encrypt(publicKey, roleStr.toString());
    }

    public static String register(String title, String adminRole, String roles,
            String publicKey, String remoteAddr) {
        Relay relay = DB.getRelay(title, remoteAddr);
        if (null == relay) {
            relay = new Relay(title, adminRole, roles, publicKey, remoteAddr);
            if (!DB.persist(relay)) {
                return "Error: RoleCall could not register " + title + " at "
                        + remoteAddr;
            }
            String[] roleNames = roles.split(",+");
            for (String roleName : roleNames) {
                Role role = DB.getRole(roleName);
                if (null == role) {
                    role = new Role(roleName);
                    if (DB.persist(role)) {
                        RelayRole relayRole = DB.getRelayRole(relay, role);
                        if (null == relayRole) {
                            relayRole = new RelayRole(relay, role);
                            DB.persist(relayRole);
                        }
                    }
                }
            }
            return Integer.toString(relay.getId());
        }
        return "Error: RoleCall already registered " + title + " at "
                + remoteAddr;
    }
}
